package com.fc.authentication.common.utils;

import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * Provides a simple wrapper around date/ time functions.
 *
 * @author Clayton Harbour (charbour@ea.com)
 *
 */
public final class DateTimeHelper
{
    /**
     * Date format used in URI or URI parameters. Example: 20100429.
     */
    public static final String URI_DATEONLY = "yyyyMMdd";
    /**
     * Date time format used in URI or URI parameters. This is to be specified in UTC (Z). Example:
     * 20100429T235123Z. We use this compact format to avoid URL encoding. HH instead of hh for
     * 24-hour clock.
     */
    public static final String URI_DATETIME = "yyyyMMdd'T'HHmmss'Z'";
    /**
     * Date format used in payload. Example: 2010-04-29.
     */
    public static final String PAYLOAD_DATEONLY = "yyyy-MM-dd";
    /**
     * Date time format used in payload. Example: 2010-04-29T23:51:23+0000. HH instead of hh for
     * 24-hour clock.
     */
    public static final String PAYLOAD_DATETIME = "yyyy-MM-dd'T'HH:mm:ssZ";
    /**
     * Date time format used in payload. Example: 2014-04-29T23:51:23T. HH instead of hh for 24-hour
     * clock.
     */
    public static final String PAYLOAD_DATETIME_MILLI = "yyyy-MM-dd'T'HH: mm:ss.SSS'Z'";
    /**
     * Date time format used in SQL DB.
     */
    public static final String SQL_DATETIME = "yyyy-MM-dd HH:mm:ss";

    public static final String QUERY_DATETIME_FORMATTER = "dd-MM-yyyy";

    public static final String PLAIN_DATETIME = "yyyyMMddHHmmss";

    public static final String PLAIN_DATE = "yyyyMMdd";
    
    /**
     * Number of milliseconds per second.
     */
    private static final long MILLISECOND_PER_SECOND = 1000L;
    private static final double MILLISECOND_PER_SECOND_FLOAT = 1000.0;

    private static final float DAY_TO_SEC_COOEF = 60 * 60 * 24;

    private DateTimeHelper()
    {
    }

    private static volatile DatatypeFactory datatypeFactory;

    private static DatatypeFactory getDatatypeFactory() // NOPMD by David.Chen to suppress
                                                        // DoubleCheckedLocking on 1/24/13 10:48 AM
    {
        if (datatypeFactory == null)
        {
            // cache a copy of this, it was showing up high on the profile list
            synchronized (DateTimeHelper.class)
            {
                if (datatypeFactory == null)
                {
                    try
                    {
                        datatypeFactory = DatatypeFactory.newInstance();
                    }
                    catch (Exception e)
                    {
                        throw new RuntimeException("date format incorrect");
                    }
                }
            }
        }
        return datatypeFactory;
    }

    /**
     * Convert to an instance of the xml gregorian calendar (for xml serialization).
     *
     * @param date
     * @return
     */
    public static XMLGregorianCalendar convertToXMLGregorianCalendar(final Date date)
    {
        final GregorianCalendar gregCal = new GregorianCalendar();
        gregCal.setTime(date);
        final DatatypeFactory dFactory = getDatatypeFactory();
        return dFactory.newXMLGregorianCalendar(gregCal);
    }

    /**
     * Convert from the xml gregorian calendar type to a java.util.Date value.
     *
     * @param calendar
     * @return
     */
    public static Date convertFromXMLGregorianCalendar(final XMLGregorianCalendar calendar)
    {
        // When we convert, we 'normalize' the value to UTC. Normally,
        // with the getTime method, this would be handled, however, due to
        // a bug in the apache libs, the cnversion fails. This, we normalize
        // it first.
        return calendar.normalize().toGregorianCalendar().getTime();
    }

    /**
     * Given the format value parse the date time from the string.
     * 
     * @param value
     * @param format
     * @return
     * @throws ParseException
     */
    public static Date parseExact(final String value, final String format) throws ParseException
    {
        final DateFormat formatter = new SimpleDateFormat(format, Locale.getDefault());
        return formatter.parse(value);
    }

    public static Integer getEpochTimeFromString(final String value) throws ParseException
    {
        final DateFormat formatter = new SimpleDateFormat(PAYLOAD_DATETIME, Locale.getDefault());
        Date date = formatter.parse(value);
        return getEpochTimeInSecond(date);
    }

    /**
     * Get a java date time given an Epoch timestamp in second (the DB format).
     *
     * @param timestamp
     */
    public static Date getDateFromEpochTimeInSecond(final long timestamp)
    {
        return new Date(timestamp * MILLISECOND_PER_SECOND);
    }

    /**
     * Get a an Epoch timestamp in second (the DB format) from java date time.
     *
     * @param date
     */
    public static int getEpochTimeInSecond(final Date date)
    {
        return (int) (date.getTime() / MILLISECOND_PER_SECOND);
    }

    /**
     * Get a an Epoch timestamp in second (the DB format) from system currentTimeMillis.
     *
     * @param date
     */
    public static int getEpochTimeInSecond(final long date)
    {
        return (int) (date / MILLISECOND_PER_SECOND);
    }

    public static String getEpochTimeToString(final int epochTime, final String format)
    {
        final Long timeMillis = epochTime * MILLISECOND_PER_SECOND;
        return getTimeMillisToString(timeMillis, format);
    }

    /**
     * Get a date (no time).
     *
     * @param year
     * @param month
     * @param day
     * @return
     */
    public static Date getDate(final int year, final int month, final int day)
    {
        final Calendar cal = Calendar.getInstance();
        cal.clear();
        cal.set(year, month, day);
        return cal.getTime();
    }

    /**
     * Get the date
     *
     * @param year
     * @param month
     * @param day
     * @param hour
     * @param minute
     * @param second
     * @return
     */
    public static Date getDate(final int year, final int month, final int day, final int hour, final int minute,
                    final int second)
    {
        final Calendar cal = Calendar.getInstance();
        cal.clear();
        cal.set(year, month, day, hour, minute, second);
        return cal.getTime();
    }

    /**
     * Get the start of the given calendar day.
     *
     * @param day
     * @return
     */
    public static Date getStartOfDay(final Date day)
    {
        final Calendar cal = Calendar.getInstance();
        cal.setTime(day);
        cal.set(Calendar.AM_PM, Calendar.AM);
        cal.set(Calendar.HOUR, cal.getActualMinimum(Calendar.HOUR));
        cal.set(Calendar.MINUTE, cal.getActualMinimum(Calendar.MINUTE));
        cal.set(Calendar.SECOND, cal.getActualMinimum(Calendar.SECOND));
        cal.set(Calendar.MILLISECOND, cal.getActualMinimum(Calendar.MILLISECOND));

        return cal.getTime();
    }

    /**
     * Get the end of the given calendar day.
     *
     * @param day
     * @return
     */
    public static Date getEndOfDay(final Date day)
    {
        final Calendar cal = Calendar.getInstance();
        cal.setTime(day);
        cal.set(Calendar.AM_PM, Calendar.PM);
        cal.set(Calendar.HOUR, cal.getActualMaximum(Calendar.HOUR));
        cal.set(Calendar.MINUTE, cal.getActualMaximum(Calendar.MINUTE));
        cal.set(Calendar.SECOND, cal.getActualMaximum(Calendar.SECOND));
        cal.set(Calendar.MILLISECOND, cal.getActualMaximum(Calendar.MILLISECOND));

        return cal.getTime();
    }

    /**
     * Get the start of week for the given calendar day.
     * 
     * @param day the day.
     * @return the start of week for the day.
     */
    public static Date getStartOfWeek(final Date day)
    {
        final Calendar cal = Calendar.getInstance();
        cal.setTime(day);
        cal.set(Calendar.DAY_OF_WEEK, cal.getActualMinimum(Calendar.DAY_OF_WEEK));
        cal.set(Calendar.AM_PM, Calendar.AM);
        cal.set(Calendar.HOUR, cal.getActualMinimum(Calendar.HOUR));
        cal.set(Calendar.MINUTE, cal.getActualMinimum(Calendar.MINUTE));
        cal.set(Calendar.SECOND, cal.getActualMinimum(Calendar.SECOND));
        cal.set(Calendar.MILLISECOND, cal.getActualMinimum(Calendar.MILLISECOND));

        return cal.getTime();
    }

    /**
     * Get the end of week for the given calendar day.
     * 
     * @param day the day.
     * @return the end of week for the given day.
     */
    public static Date getEndOfWeek(final Date day)
    {
        final Calendar cal = Calendar.getInstance();
        cal.setTime(day);
        cal.set(Calendar.DAY_OF_WEEK, cal.getActualMaximum(Calendar.DAY_OF_WEEK));
        cal.set(Calendar.AM_PM, Calendar.PM);
        cal.set(Calendar.HOUR, cal.getActualMaximum(Calendar.HOUR));
        cal.set(Calendar.MINUTE, cal.getActualMaximum(Calendar.MINUTE));
        cal.set(Calendar.SECOND, cal.getActualMaximum(Calendar.SECOND));
        cal.set(Calendar.MILLISECOND, cal.getActualMaximum(Calendar.MILLISECOND));

        return cal.getTime();
    }

    /**
     * Get the start of month for the given calendar day.
     * 
     * @param day the day.
     * @return the start of month for the given day.
     */
    public static Date getStartOfMonth(final Date day)
    {
        final Calendar cal = Calendar.getInstance();
        cal.setTime(day);
        cal.set(Calendar.DAY_OF_MONTH, cal.getActualMinimum(Calendar.DAY_OF_MONTH));
        cal.set(Calendar.AM_PM, Calendar.AM);
        cal.set(Calendar.HOUR, cal.getActualMinimum(Calendar.HOUR));
        cal.set(Calendar.MINUTE, cal.getActualMinimum(Calendar.MINUTE));
        cal.set(Calendar.SECOND, cal.getActualMinimum(Calendar.SECOND));
        cal.set(Calendar.MILLISECOND, cal.getActualMinimum(Calendar.MILLISECOND));

        return cal.getTime();
    }

    /**
     * Get the end of month for the given calendar day.
     * 
     * @param day the day.
     * @return the end of month for the given day.
     */
    public static Date getEndOfMonth(final Date day)
    {
        final Calendar cal = Calendar.getInstance();
        cal.setTime(day);
        cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
        cal.set(Calendar.AM_PM, Calendar.PM);
        cal.set(Calendar.HOUR, cal.getActualMaximum(Calendar.HOUR));
        cal.set(Calendar.MINUTE, cal.getActualMaximum(Calendar.MINUTE));
        cal.set(Calendar.SECOND, cal.getActualMaximum(Calendar.SECOND));
        cal.set(Calendar.MILLISECOND, cal.getActualMaximum(Calendar.MILLISECOND));

        return cal.getTime();
    }

    /**
     * Get next day (add Day)of the given calendar day.
     *
     * @param day
     * @return
     */
    public static Date getNextDay(final Date day)
    {
        final Calendar cal = Calendar.getInstance();
        cal.setTime(day);
        cal.add(Calendar.DATE, 1);
        return cal.getTime();
    }

    public static Date addDays(final Date originalDay, final int numDays)
    {
        final Calendar cal = Calendar.getInstance();
        cal.setTime(originalDay);
        cal.add(Calendar.DAY_OF_MONTH, numDays);
        final Date newDay = new Date(cal.getTimeInMillis());

        return newDay;
    }

    public static Date addHours(final Date originalDay, final int numHours)
    {
        final Calendar cal = Calendar.getInstance();
        cal.setTime(originalDay);
        cal.add(Calendar.HOUR_OF_DAY, numHours);
        final Date newDay = new Date(cal.getTimeInMillis());

        return newDay;
    }

    public static Date getMax(final Date day1, final Date day2)
    {
        if (day1 == null)
        {
            return day2;
        }
        if (day2 == null)
        {
            return day1;
        }
        final Calendar cal1 = Calendar.getInstance();
        final Calendar cal2 = Calendar.getInstance();

        cal1.setTime(day1);
        cal2.setTime(day2);

        if (cal1.before(cal2))
        {
            return cal2.getTime();
        }
        else
        {
            return cal1.getTime();
        }
    }

    /**
     * Takes a date and a date format string and returns a string of the date formatted according to
     * the passed in arguments
     *
     * @param date - date to turn into a string
     * @param dateFormatString - string specifying how the date is to be formatted into a string
     * @return String - the date argument in a string representation according to the passed in
     *         dateFormatString
     */
    public static String getStringFromDate(final Date date, final String dateFormatString)
    {
        final SimpleDateFormat sdf = new SimpleDateFormat(dateFormatString, Locale.getDefault());
        return sdf.format(date);
    }

    /**
     * Takes a date in string format and a date format string and returns a Date
     *
     * @param dateString - the date in a string representation
     * @param dateFormatString - the format of how the date is represented in the dateString
     *            argument
     * @return Date - returns a date object as specified by the dateString argument
     */
    public static Date getDateFromString(final String dateString, final String dateFormatString)
    {
        try
        {
            return parseExact(dateString, dateFormatString);
        }
        catch (ParseException e)
        {
            throw new RuntimeException("Date could not be parsed in the specified format: "
                            + dateFormatString + " - " + e.getMessage());
        }
    }

    /**
     * Returns weather the date is within a given range.
     *
     * @return
     */
    public static boolean isDateInRange(final Date dateToCheck, final Date startDate, final Date endDate)
    {
        return !((startDate != null && dateToCheck.before(startDate))
                        || (endDate != null && dateToCheck.after(endDate)));
    }

    /**
     * Returns weather the date is within a given range. - Governed by one field
     *
     * @return
     */
    public static boolean isDateInRange(final Date dateToCheck, final Date midRange, final Integer startRange,
                    final Integer endRange, final int fieldType)
    {

        Date startDate = null;
        if (startRange != null)
        {
            final Calendar startCal = Calendar.getInstance();
            startCal.setTime(midRange);
            startCal.add(fieldType, startRange);
            startDate = startCal.getTime();
        }

        Date endDate = null;
        if (endRange != null)
        {
            final Calendar endCal = Calendar.getInstance();
            endCal.setTime(midRange);
            endCal.add(fieldType, endRange);
            endDate = endCal.getTime();
        }

        return isDateInRange(dateToCheck, startDate, endDate);
    }

    public static long getDiffInSeconds(final Date firstDay, final Date secondDay)
    {
        final long diffInMsec = Math.abs(firstDay.getTime() - secondDay.getTime());
        final long diffInSec = Math.round(diffInMsec / MILLISECOND_PER_SECOND_FLOAT); // just round
                                                                                      // the double
                                                                                      // to seconds

        return diffInSec;
    }

    /**
     * @param seconds
     * @return
     */
    public static double convertSecondsToDays(final long seconds)
    {
        return seconds / DAY_TO_SEC_COOEF;
    }

    /**
     * @param days
     * @return
     */
    public static double convertDaysToSeconds(final long days)
    {
        return days * DAY_TO_SEC_COOEF;
    }

    /**
     * @param timeMillis
     * @param format
     * @return
     */
    public static String getTimeMillisToString(final long timeMillis, final String format)
    {
        final SimpleDateFormat dateformatter = new SimpleDateFormat(format, Locale.getDefault());
        dateformatter.setTimeZone(TimeZone.getTimeZone(System.getProperty("user.timezone")));
        return dateformatter.format(new Date(timeMillis));
    }

    public static int getMonthEpochTime(final Integer epochTime)
    {
        String startString = DateTimeHelper.getEpochTimeToString(epochTime, DateTimeHelper.PAYLOAD_DATEONLY);
        String monthStr = startString.substring(5, 7);
        if (monthStr.startsWith(""))
        {
            monthStr = monthStr.substring(1, 2);
        }
        return Integer.valueOf(monthStr);
    }

    public static String getYearAndMonthFromTimeMillis(final Integer epochTime)
    {
        String startString = DateTimeHelper.getEpochTimeToString(epochTime, DateTimeHelper.PAYLOAD_DATEONLY);

        return startString.substring(0, 7);
    }


    public static  int calculateDateDiff(Date from, Date to)
    {
        long timestamp = to.getTime() - from.getTime();
        return (int)(timestamp / (1000 * 60 * 60 * 24));
    }
}
